16.3 分配
为对象分配内存须区分是在栈还是堆上完成。通常情况下,编译器有责任尽可能使用寄存器和栈来存储对象,这有助于提升性能,减少垃圾回收器的压力。
但千万不要以为用了new函数就一定会分配在堆上,即便是相同的源码也有不同的结果。
test.go
package main
import()
func test() *int{ x:=new(int) *x=0xAABB return x }
func main() { println(*test()) }
当编译器禁用内联优化时,所生成代码和我们的源码表面上预期一致。
$go build-gcflags”-l” -o test test.go // 关闭内联优化
$go tool objdump-s”main.test”test
TEXT main.test(SB)test.go test.go:5 SUBQ0xaabb,0(AX) test.go:8 MOVQ AX,0x18(SP) test.go:8 ADDQ$0x10,SP test.go:8 RET
但当使用默认参数时,函数test会被main内联,此时结果就变得不同了。
$go build-o test test.go // 默认优化
$go tool objdump-s”main.main”test
TEXT main.main(SB)test.go test.go:11 SUBQ0x0,0x10(SP) test.go:12 LEAQ 0x10(SP),BX test.go:12 MOVQ0x18,SP test.go:13 RET
看不懂汇编没关系,但显然内联优化后的代码没有调用newobject在堆上分配内存。
编译器这么做,道理很简单。没有内联时,需要在两个栈帧间传递对象,因此会在堆上分配而不是返回一个失效栈帧里的数据。而当内联后,它实际上就成了main栈帧内的局部变量,无须去堆上操作。
Go编译器支持逃逸分析(escape analysis),它会在编译期通过构建调用图来分析局部变量是否会被外部引用,从而决定是否可直接分配在栈上。
编译参数-gcflags”-m”可输出编译优化信息,其中包括内联和逃逸分析。
你或许见过“Zero Garbage”这个说法,其目的就是避免在堆上的分配行为,从而减小垃圾回收压力,提升性能。另外,做性能测试时使用go test-benchmem参数可以输出堆分配次数统计。
好了,本章要关注的是内存分配器,而非编译器。借着上面这个例子,我们开始深入挖掘newobject具体是如何为对象分配内存的。
mcache.go
type mcache struct{ //Allocator cache for tiny objects w/o pointers. tiny unsafe.Pointer tinyoffset uintptr
alloc[_NumSizeClasses]*mspan }
malloc.go
// 内置函数new实现 func newobject(typ*_type)unsafe.Pointer{ return mallocgc(uintptr(typ.size),typ,flags) }
func mallocgc(size uintptr,typ*_type,flags uint32)unsafe.Pointer{ // 当前线程所绑定的cache c:=gomcache()
// 小对象 if size⇐maxSmallSize{ // 无须扫描非指针微小对象(小于16) if flags&flagNoScan!=0&&size<maxTinySize{ off:=c.tinyoffset
// 对齐,调整偏移量
if size&7==0{
off=round(off,8)
}else if size&3==0{
off=round(off,4)
}else if size&1==0{
off=round(off,2)
}
// 如果剩余空间足够...
if off+size<=maxTinySize&&c.tiny!=nil{
// 返回指针,调整偏移量为下次分配做好准备
x=add(c.tiny,off)
c.tinyoffset=off+size
return x
}
// 获取新的tiny块
// 就是从sizeclass=2的span.freelist获取一个16字节object
s=c.alloc[tinySizeClass]
v:=s.freelist
// 如果没有可用的object,那么需要从central获取新的span
if v.ptr() ==nil{
systemstack(func() {
mCache_Refill(c,tinySizeClass)
})
// 重新提取tiny块
s=c.alloc[tinySizeClass]
v=s.freelist
}
// 提取object后,调整span.freelist链表,增加使用计数
s.freelist=v.ptr().next
s.ref++
// 初始化(零值)tiny块
x=unsafe.Pointer(v)
(*[2]uint64)(x)[0] =0
(*[2]uint64)(x)[1] =0
// 对比新旧两个tiny块的剩余空间
// 新块分配后,其tinyoffset=size,因此比对偏移量即可
if size<c.tinyoffset{
// 用新块替换
c.tiny=x
c.tinyoffset=size
}
// 消费一个新的完整tiny块
size=maxTinySize
}else{
// 普通小对象
// 查表,以确定sizeclass
var sizeclass int8
if size<=1024-8{
sizeclass=size_to_class8[(size+7)>>3]
}else{
sizeclass=size_to_class128[(size-1024+127)>>7]
}
size=uintptr(class_to_size[sizeclass])
// 从对应规格的span.freelist提取object
s=c.alloc[sizeclass]
v:=s.freelist
// 没有可用的object,从central获取新的span
if v.ptr() ==nil{
systemstack(func() {
mCache_Refill(c,int32(sizeclass))
})
// 重新提取object
s=c.alloc[sizeclass]
v=s.freelist
}
// 调整span.freelist链表,增加使用计数
s.freelist=v.ptr().next
s.ref++
// 清零(变量默认总是初始化为零值)
x=unsafe.Pointer(v)
if flags&flagNoZero==0{
v.ptr().next=0
if size>2*ptrSize&& ((*[2]uintptr)(x))[1] !=0{
memclr(unsafe.Pointer(v),size)
}
}
}
}else{ // 大对象直接从heap分配span var s*mspan systemstack(func() { s=largeAlloc(size,uint32(flags)) })
//span.start实际由address>>pageShift生成
x=unsafe.Pointer(uintptr(s.start<<pageShift))
size=uintptr(s.elemsize)
}
// 在bitmap做标记 … // 检查触发条件,启动垃圾回收 …
return x }
整理一下这段代码的基本思路:
- 大对象直接从heap获取span。
- 小对象从cache.alloc[sizeclass].freelist获取object。
- 微小对象组合使用cache.tiny object。
对微小对象的处理很有意思。首先,它不能是指针,因为多个小对象被组合到一个object里,显然无法应对垃圾扫描。其次,它从span.freelist获取一个16字节的object,然后利用偏移量来记录下一次分配的位置。
这里有个小细节,体现了作者的细心。当tiny因剩余空间不足而使用新object时,会比较新旧两个tiny object的剩余空间,而非粗暴地喜新厌旧。
分配算法本身并不复杂,没什么好说的,接下来要关注的自然是资源不足时如何扩张。考虑到大对象分配过程没有central这个中间环节,所以我们先跳largeAlloc这个坑。
malloc.go
func largeAlloc(size uintptr,flag uint32) *mspan{ // 计算所需页数 npages:=size>> _PageShift if size&_PageMask!=0{ npages++ }
// 清理(sweep)垃圾 …
// 从heap获取span,并重置在bitmap里的标记 s:=mHeap_Alloc(&mheap_,npages,0,true,flag&_FlagNoZero==0) heapBitsForSpan(s.base()).initSpan(s.layout())
return s }
先不用跟过去看mHeap_Alloc,因为小对象扩张函数mCache_Refill最终也会调用它。
mcache.go
func mCache_Refill(c*mcache,sizeclass int32) *mspan{ // 放弃当前正在使用的span(尚在central.empty里) s:=c.alloc[sizeclass] if s!= &emptymspan{ s.incache=false // 取消“正在使用”标志 }
// 从central获取span进行替换 s=mCentral_CacheSpan(&mheap_.central[sizeclass].mcentral) c.alloc[sizeclass] =s return s }
在跳转到central之前,先得了解sweepgen这个概念。垃圾回收每次都会累加这个类似代龄的计数值,而每个等待处理的span也有该属性。
mheap.go
type mheap struct{ sweepgen uint32 //sweep generation,see comment in mspan sweepdone uint32 //all spans are swept }
type mspan struct{ //if sweepgenh->sweepgen-2,the span needs sweeping //if sweepgenh→sweepgen-1,the span is currently being swept //if sweepgen==h→sweepgen, the span is swept and ready to use //h→sweepgen is incremented by 2 after every GC sweepgen uint32 }
在heap里闲置的span不会被垃圾回收器关注,但central里的span却有可能正在被清理。所以当cache从central提取span时,该属性值就非常重要。
mcentral.go
type mcentral struct{ nonempty mspan // 链表:span尚有空闲object可用 empty mspan // 链表:span没有空闲object可用,或已被cache取走 }
func mCentral_CacheSpan(c*mcentral) *mspan{ // 清理(sweep)垃圾 …
sg:=mheap_.sweepgen
retry: // 遍历nonempty链表 for s=c.nonempty.next;s!= &c.nonempty;s=s.next{ // 需要清理的span if s.sweepgen==sg-2&&cas(&s.sweepgen,sg-2,sg-1) { // 因为要交给cache使用,所以转移到empty链表 mSpanList_Remove(s) mSpanList_InsertBack(&c.empty,s)
// 垃圾清理
mSpan_Sweep(s,true)
goto havespan
}
// 忽略正在清理的span
if s.sweepgen==sg-1{
continue
}
// 已清理过的span
mSpanList_Remove(s)
mSpanList_InsertBack(&c.empty,s)
goto havespan
}
// 遍历empty链表 for s=c.empty.next;s!= &c.empty;s=s.next{ // 需要清理的span if s.sweepgen==sg-2&&cas(&s.sweepgen,sg-2,sg-1) { mSpanList_Remove(s) mSpanList_InsertBack(&c.empty,s) mSpan_Sweep(s,true)
// 清理后有可用的object
if s.freelist.ptr() !=nil{
goto havespan
}
// 清理后依然没可用的object,重试
goto retry
}
// 忽略正在清理的span
if s.sweepgen==sg-1{
continue
}
// 已清理过,且不为空的span都被转移到noempty链表
// 这里剩下的自然都是全空或正在被cache使用的,继续循环已没有意义
break
}
// 如果两个链表里都没span可用,扩张 s=mCentral_Grow(c) // 新span将被cache使用,所以放到empty链表尾部 mSpanList_InsertBack(&c.empty,s)
havespan: // 设置被cache使用标志 s.incache=true
return s }
可以看出,从central里获取span时,优先取用已有资源,哪怕是要执行清理操作。只有当现有资源都无法满足时,才去heap获取span,并重新切分成object链表。
mcentral.go
func mCentral_Grow(c*mcentral) *mspan{ // 查表获取所需页数 npages:=uintptr(class_to_allocnpages[c.sizeclass]) size:=uintptr(class_to_size[c.sizeclass])
// 计算切分object数量 n:= (npages<< _PageShift) /size
// 从heap获取span s:=mHeap_Alloc(&mheap_,npages,c.sizeclass,false,true)
// 切分成object链表 p:=uintptr(s.start<< _PageShift) // 内存地址 head:=gclinkptr(p) tail:=gclinkptr(p) for i:=uintptr(1);i<n;i++ { p+=size tail.ptr().next=gclinkptr(p) tail=gclinkptr(p) } tail.ptr().next=0 s.freelist=head
// 重置在bitmap里的标记 heapBitsForSpan(s.base()).initSpan(s.layout())
return s }
好了,现在大小对象殊途同归,都到了mHeap_Alloc这里。
mheap.go
type mheap struct{ busy [_MaxMHeapList]mspan // 链表数组:已分配大对象span,127页以内 busylarge mspan // 链表:已分配超过127页大对象span }
func mHeap_Alloc(h*mheap,npage uintptr,sizeclass int32,large bool,needzero bool) *mspan{ systemstack(func() { s=mHeap_Alloc_m(h,npage,sizeclass,large) })
// 对span清零
return s }
func mHeap_Alloc_m(h*mheap,npage uintptr,sizeclass int32,large bool) *mspan{
// 清理(sweep)垃圾 …
// 从heap获取指定页数的span s:=mHeap_AllocSpanLocked(h,npage) if s!=nil{ // 重置span状态 atomicstore(&s.sweepgen,h.sweepgen) s.state= _MSpanInUse s.freelist=0 s.ref=0 s.sizeclass=uint8(sizeclass) …
// 小对象取用的span被存放到central.empty链表
// 而大对象所取用的span则放在heap.busy链表
if large{
// 根据页数来判断将其放到busy还是busylarge链表
// 数组free使用页数作为索引,那么len(free)就是最大页数边界
if s.npages<uintptr(len(h.free)) {
mSpanList_InsertBack(&h.busy[s.npages],s)
}else{
mSpanList_InsertBack(&h.busylarge,s)
}
}
}
return s }
从heap获取span的算法核心是找到大小最合适的块。首先从页数相同的链表查找,如没有结果,再从页数更多的链表提取,直至超大块或申请新块。
如返回更大的span,为避免浪费,会将多余部分切出来重新放回heap链表。同时还尝试合并相邻的闲置span空间,减少碎片。
mheap.go
type mheap struct{ free [_MaxMHeapList]mspan // 链表数组:页数127以内的闲置span freelarge mspan // 链表:页数大于127的闲置span }
func mHeap_AllocSpanLocked(h*mheap,npage uintptr) *mspan{ // 先尝试获取指定页数的span,不行就试页数更多的 for i:=int(npage);i<len(h.free);i++ { // 从链表取span if!mSpanList_IsEmpty(&h.free[i]) { s=h.free[i].next goto HaveSpan } }
// 再不行,就试一试页数超过127的超大span s=mHeap_AllocLarge(h,npage)
// 还没有,就得从操作系统申请新的了 if s==nil{ if!mHeap_Grow(h,npage) { return nil }
// 因为每次申请最小1MB/128Pages,所以被放到freelarge链表,再试
s=mHeap_AllocLarge(h,npage)
if s==nil{
return nil
}
}
HaveSpan: // 从free链表移除 mSpanList_Remove(s)
// 如果该span曾被释放物理内存,重新映射补回 …
// 如果该span页数多于预期 … if s.npages>npage{ // 创建新span用来管理多余的内存 t:= (*mspan)(fixAlloc_Alloc(&h.spanalloc)) mSpan_Init(t,s.start+pageID(npage),s.npages-npage)
// 调整切割后的页数
s.npages=npage
// 将新建span放回heap
mHeap_FreeSpanLocked(h,t,false,false,s.unusedsince)
}
// 在spans填充全部指针 p:=uintptr(s.start) p-= (uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift) for n:=uintptr(0);n<npage;n++ { h_spans[p+n] =s }
return s }
因为freelarge只是一个简单链表,没有页数做索引,也不曾按大小排序,所以只能遍历整个链表,然后选出最小、地址最靠前的块。
mheap.go
func mHeap_AllocLarge(h*mheap,npage uintptr) *mspan{ return bestFit(&h.freelarge,npage,nil) }
func bestFit(listmspan,npage uintptr,bestmspan) *mspan{
for s:=list.next;s!=list;s=s.next{
if s.npages<npage{
continue
}
if bestnil||s.npages
至于将span放回heap的mHeap_FreeSpanLocked操作,将在内存回收章节再做详述。而在内存分配阶段,也只剩向操作系统申请新内存块了。
mheap.go
func mHeap_Grow(h*mheap,npage uintptr)bool{ // 大小总是64KB的倍数,最少1MB npage=round(npage, (64<<10)/_PageSize) ask:=npage<< _PageShift if ask< _HeapAllocChunk{ ask= _HeapAllocChunk }
// 向操作系统申请内存 v:=mHeap_SysAlloc(h,ask)
// 创建span用来管理刚申请的内存 s:= (*mspan)(fixAlloc_Alloc(&h.spanalloc)) mSpan_Init(s,pageID(uintptr(v)>>_PageShift),ask>>_PageShift)
// 填充在spans区域的信息 p:=uintptr(s.start) p-= (uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift) for i:=p;i<p+s.npages;i++ { h_spans[i] =s }
// 放到heap相关链表中 mHeap_FreeSpanLocked(h,s,false,true,0)
return true }
依然是用mmap从指定位置申请内存。最重要的是同步扩张bitmap和spans区域,以及调整arena_used这个位置指示器。
malloc.go
func mHeap_SysAlloc(h*mheap,n uintptr)unsafe.Pointer{ // 不能超出arena大小限制 if n⇐uintptr(h.arena_end)-uintptr(h.arena_used) { // 从指定位置申请内存 p:=h.arena_used sysMap((unsafe.Pointer)(p),n,h.arena_reserved, &memstats.heap_sys)
// 同步扩张bitmap和spans内存
mHeap_MapBits(h,p+n) mHeap_MapSpans(h,p+n)
// 调整下一次申请地址
h.arena_used=p+n
return(unsafe.Pointer)(p)
} }
mem_linux.go
func sysMap(v unsafe.Pointer,n uintptr,reserved bool,sysStat*uint64) { if!reserved{ p:=mmap_fixed(v,n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1,0) return }
p:=mmap(v,n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1,0) }
至此,内存分配操作流程正式结束。